﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TheCopticProject.CopticCalendar
{
    public class Date
    {
        #region Private Members
        private int _year = 1;
        private int _month = 1;
        private int _day = 1;
        #endregion

        #region Constants
        public const int MinMonth = 1;
        public const int MaxMonth = 13;
        public const int MinDay = 1;
        public const int MaxDayInDefaultMonths = 30;
        public const int MaxDayInKogiMonthDefaultYear = 5;
        public const int MaxDayInKogiMonthLeapYear = 6;
        public const int LeapYearFrequency = 4;

        public const int DefaultYearDays = (MaxMonth - 1) * MaxDayInDefaultMonths + MaxDayInKogiMonthDefaultYear;
        public const int LeapYearDays = (MaxMonth - 1) * MaxDayInDefaultMonths + MaxDayInKogiMonthLeapYear;
        public const int FourYearsDays = (LeapYearFrequency - 1) * DefaultYearDays + LeapYearDays;

        public static readonly DateTime GregorianDateForFirstDayOfFirstCopticYear = new DateTime(284, 8, 29);

        public static readonly string[] MonthNames = new string[] { "Thout",
            "Paopi",
            "Hathor",
            "Koiak",
            "Tobi",
            "Meshir",
            "Paremhat",
            "Paremoude",
            "Pashons",
            "Paoni",
            "Epip",
            "Mesori",
            "Pi Kogi Enavot" };
        #endregion

        #region Constructors
        public Date(int year, int month, int day)
        {
            _year = year;
            _month = month;
            _day = day;
            Validate();
        }

        public Date(DateTime gregorianDate)
        {
            SetFromGregorianDate(gregorianDate);
        }
        #endregion

        #region Public Properties
        public int Year { get { return _year; } set { ValidateYear(value); _year = value; } }
        public int Month { get { return _month; } set { ValidateMonth(value); _month = value; } }
        public int Day { get { return _day; } set { ValidateDay(value, _month, _year); _day = value; } }

        /// <summary>
        /// Returns absolute day in the current year
        /// The returned number is 1 based, which means the first day in the year corresponds to 1.
        /// Zero is not a valid return value of this property
        /// </summary>
        public int DayInYear { get { return (_month - 1) * MaxDayInDefaultMonths + _day; } }

        /// <summary>
        /// Returns the corresponding Gregorian calendar date
        /// </summary>
        public DateTime ToGregorianDate { get { return GregorianDateForFirstDayOfFirstCopticYear.AddDays(AbsoluteDays - 1); } }

        /// <summary>
        /// Returns days since 1 Thout year 1.
        /// The value returned is 1 based, which means a value of 1 would represent the first day of the first year.
        /// Zero is not a valid return value of this property
        /// </summary>
        public int AbsoluteDays
        {
            get
            {
                int days = ((_year - 1) / LeapYearFrequency) * FourYearsDays + DayInYear;

                for (int year = _year - 1; year >= _year - ((_year - 1) % LeapYearFrequency); year--)
                    days += GetYearDays(year);

                return days;
            }
        }
        #endregion

        #region Public Methods
        public Date SubtractDays(int days)
        {
            if (days < 0)
                throw new ArgumentException("Days to subtract must be positive non-zero number", "days");

            Date result = CreateClone();
            result.SetFromAbsoluteDays(AbsoluteDays - days - 1);
            return result;
        }

        public Date AddDays(int days)
        {
            Date result = CreateClone();
            result.InternalAddDays(days);
            return result;
        }

        public override bool Equals(object obj)
        {
            Date other = (Date)obj;
            return _year == other._year && _month == other._month && _day == other._day;
        }

        public override string ToString()
        {
            return string.Format("{0} {1} {2}", _day, GetMonthName(_month), _year);
        }
        #endregion

        #region Public Static Utilities
        public static Date Now { get { return new Date(DateTime.Now); } }

        public static int GetMonthDays(int month, int year)
        {
            ValidateYear(year);
            ValidateMonth(month);

            if (month == MaxMonth)
                return IsLeapYear(year) ? MaxDayInKogiMonthLeapYear : MaxDayInKogiMonthDefaultYear;
            else
                return MaxDayInDefaultMonths;
        }

        public static int GetYearDays(int year)
        {
            return IsLeapYear(year) ? LeapYearDays : DefaultYearDays;
        }

        public static bool IsLeapYear(int year)
        {
            ValidateYear(year);
            return (year + 1) % LeapYearFrequency == 0;
        }

        public static string GetMonthName(int month)
        {
            ValidateMonth(month);
            return MonthNames[month - 1];
        }

        public static Date GetEasterDate(int year)
        {
            int gregorianYear1 = new Date(year, MaxMonth, GetMonthDays(MaxMonth, year)).ToGregorianDate.Year;

            Date date = new Date(EasterDateCalculator.GetEasterDate(gregorianYear1, EasterDateCalculator.CalculationType.Orthodox));

            if (date.Year == year)
                return date;

            int gregorianYear2 = new Date(year, MinMonth, MinDay).ToGregorianDate.Year;

            date = new Date(EasterDateCalculator.GetEasterDate(gregorianYear2, EasterDateCalculator.CalculationType.Orthodox));

            if (date.Year == year)
                return date;

            throw new InvalidOperationException("Couldn't get easter date for year");
        }
        #endregion

        #region Private Utilities
        private Date CreateClone()
        {
            return new Date(_year, _month, _day);
        }

        /// <summary>
        /// Set this date object's date using absolute days from the staring of Coptic calendar.
        /// </summary>
        /// <param name="absoluteDays">Absolute zero based days from 1 Thout year 1. First day is represented by the value 0. The value 1 is day 2 Thout year 1, etc.</param>
        private void SetFromAbsoluteDays(int absoluteDays)
        {
            _year = 1;
            _month = 1;
            _day = 1;
            InternalAddDays(absoluteDays);
        }

        private void SetFromGregorianDate(DateTime gregorianDate)
        {
            SetFromAbsoluteDays(gregorianDate.Subtract(GregorianDateForFirstDayOfFirstCopticYear).Days);
        }

        private void InternalAddDays(int days)
        {
            if (days < 0)
                throw new ArgumentException("Days to add must be positive non-zero number", "days");

            days += DayInYear - 1;

            _year += (days / FourYearsDays) * LeapYearFrequency;
            days %= FourYearsDays;

            for (int currentYearDays = GetYearDays(_year); days >= currentYearDays; currentYearDays = GetYearDays(_year))
            {
                _year++;
                days -= currentYearDays;
            }

            _month = days / MaxDayInDefaultMonths + 1;
            _day = days % MaxDayInDefaultMonths + 1;
        }

        private void Validate()
        {
            Validate(_year, _month, _day);
        }

        private static void Validate(int year, int month, int day)
        {
            ValidateYear(year);
            ValidateMonth(month);
            ValidateDay(day, month, year);
        }

        private static void ValidateYear(int year)
        {
            if (year < 1)
                throw new InvalidOperationException("Year must be a positive number greater than zero");
        }

        private static void ValidateMonth(int month)
        {
            if (month < MinMonth || month > MaxMonth)
                throw new InvalidOperationException(string.Format("Month should be between {0} and {1}", MinMonth, MaxMonth));
        }

        private static void ValidateDay(int day, int month, int year)
        {
            if (day < MinDay || day > GetMonthDays(month, year))
                throw new InvalidOperationException(string.Format("Day in month {0} of year {1} should be between {2} and {3}", month, year, MinDay, GetMonthDays(month, year)));
        }
        #endregion
    }
}
